1 影音元数据

1)首先介绍一个影音元数据库的网站TMDB。

https://www.themoviedb.org/

注册账号并获取api的key

2)字幕网站

https://www.opensubtitles.com/

注册账号并获取api的key

3)关于刮削的文件命名

基本信息:

原剧集语言
固定演员

参考:

https://www.bilibili.com/read/cv3275648/

https://www.luxiyue.com/personal/%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82%E7%94%B5%E5%BD%B1%E6%96%87%E4%BB%B6%E5%91%BD%E5%90%8D%E8%A7%84%E5%88%99/

https://blog.iplayloli.com/sharing-of-tinymeediamanger-renaming-rules.html

8.4 影视刮削工具

下载刮削工具 TinyMediaManager

下载地址:https://www.tinymediamanager.org/

批量重命名工具 Advance Renamer

下载地址:https://www.advancedrenamer.com/

assets/nas017.png

assets/nas019.png

8.2 影音命名规则

影音分为两种。
1)MOVIE:电影
2)TVSHOW:剧集(例如连续剧或者番剧)

8.2.1电影命名规则

文件夹命名为
影片名称(年份)

电影命名:
电影中文名.电影原始名.发行年份.分辨率.影片来源.影片编码.影片格式
加勒比海盗2:聚魂棺.Pirates.of.the.Caribbean.Dead.Mans.Chest.2006.BluRay.720p.x264.AC3-WOFEI.mkv

8.2.1剧集命名规则

Friends.S03E10.1080p.x264.mkv
节目名称.第几季第几集.分辨率.影片编码.视频格式

8.3 字幕编辑

subtitleEdit 字幕编辑

.default.字母格式: 在文件名末尾附加字幕来将字幕标记为默认字幕

8.4 刮削后整理:重命名

8.4.1 电影
文件夹重命名配置:

${title}${ - ,edition,} (${,year,})

文件命名配置:
${title}${if !movie.title=movie.originalTitle}${.originalTitle}${end}${.,year,}${.,edition,}${.,mediaSource,}${.,videoFormat,}${.,videoCodec,}.${videoBitDepth}bit.${.,audioCodec;upper,}

assets/nas020.png

8.4.2电视

1)文件夹重命名配置:
${showTitle} (${showYear})

2)特别篇或者OVA目录
文件命名配置:
Specials

3)季命名
Season ${seasonNr}

3)文件命名
${tvShow.title}${if !tvShow.title=tvShow.originalTitle}${.,tvShow.originalTitle,}${end}.S${seasonNr2}E${episodeNr2}${.,videoFormat,;replace(format.csv)}${.,videoCodec,;replace(codec.csv)}

第几季第几集用S季数E集数表示
例如,第八季第五集: S08E05

  1. 特别篇或者OVA目录文件
    季命名以S00开始

好的文件命名是决定刮削是否准确的关键

效果如图

assets/nas018.png

1.9 番号整理

1.9.1 软件下载安装

使用MDXC进行番号刮削

项目地址:
https://github.com/sqzw-x/mdcx

下载地址:
https://github.com/sqzw-x/mdcx/releases/tag/120240321

配置如下:

assets/nas013.png

assets/nas014.png

assets/nas015.png

assets/nas016.png

具体不能多说了,自己研究吧。展示下刮削效果

assets/nas021.png

assets/nas022.png

1.9.2 番号命名规则

刮削前文件名称整理

  1. 目录名称不动
  2. 文件名称修改为番号名称

刮削重命名配置

  1. 目录名称不动
  2. 文件名称修改为番号名称

音乐刮削

下载Music Tag音乐标签客户端
(或者Mp3Tag,需要自己配置源)

文件-添加目录:添加音乐目录

工具栏-点击自动匹配标签:自动匹配项,除了标题和艺术家都选上

文件-重命名:按照 (艺术家 - 标题) 命名

1.9 番号整理

1.9.1 软件下载安装

使用MDXC进行番号刮削

项目地址:
https://github.com/sqzw-x/mdcx

下载地址:
https://github.com/sqzw-x/mdcx/releases/tag/120240321

配置如下:

assets/nas013.png

assets/nas014.png

assets/nas015.png

assets/nas016.png

具体不能多说了,自己研究吧。展示下刮削效果

assets/nas021.png

assets/nas022.png

1.9.2 番号命名规则

刮削前文件名称整理

  1. 目录名称不动
  2. 文件名称修改为番号名称

刮削重命名配置

  1. 目录名称不动
  2. 文件名称修改为番号名称

1 方案说明

硬件搭配方便,主要做出了一下三种硬件方案。

  1. 省电方案
硬件 型号 价格
CPU 8100 180
主板 华南B250 250
内存 玖合DDR4-8GB X2 200
m2固态 铠侠SD10-1T 480
电源 ds-atx 100
电源 150dc电源 100
机箱 盘隆-4盘位机箱 260

8100带T和不带T,待机情况下功耗没有区别。
待机功耗。以上13W。

2 硬盘柜版

说明。采用小主机+usb+外接硬盘柜方案。

硬件 型号 价格
CPU 七喜N100准系统小主机 700
内存 玖合dd4笔记本内存3200-16G 180
m2固态 铠侠SD10-1T 480
硬盘柜 奥睿科5盘位硬盘柜-无RAID 800
总计 2160元

待机功耗。以上20W。

光标寄存器

光标位置信息位于显卡的2个光标寄存器中,总共16位,分为高位和低位存储。

例如。

标准VGA文本模式为 25 行 80 列

例如:
pos = 0 :表示 位于 1 行 0 列
pos = 27 :表示 位于 2 行 2 列
pos =1999 : 最右下角

读取光标值

光标寄存器的端口号是 0x3d4 和 0x3d5

0x3d4用来写入变量
0x3d5用来获取光标的值

端口 读写
0x3d4 0xe0 写入
0x3d5 光标高8位值 读取
0x3d4 0xf0 写入
0x3d5 光标低8位值 读取

获取光标和设置光标

1
2
3
4
uint16 get_cursor();

void set_cursor(uint16 pos);

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint16 get_cursor()
{
//get high cursor value
io_out8(0x3d4, 0x0e);
uint8 cursor_high = io_in8(0x3d5);
//get low cursor value
io_out8(0x3d4, 0x0f);
uint8 cursor_low = io_in8(0x3d5);
//high + low
return (cursor_high << 8) + (cursor_low & 0xff);
}

void set_cursor(uint16 pos)
{
uint8 cursor_high = pos >> 8;
uint8 cursor_low = pos & 0xff;
//set high cursor value
io_out8(0x3d4, 0x0e);
io_out8(0x3d5, cursor_high);
//set low cursor value
io_out8(0x3d4, 0x0f);
io_out8(0x3d5, cursor_low);
}

说明:
pos代表设置位置,标准VGA文本模式为 25 行 80 列,pos取值从0到1999. 通过get_cursor函数获取光标现在的位置,通过set_cursor设置光标在25 行 80 列文本模式的位置。

外设端口

设备通常会提供一组寄存器来控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。

这些寄器可能位于I/O空间中,也可能位于内存空间中。当位于I/O空间时,通常被称为I/O端口;当位于内存空间时,对应的内存空间被称为I/O内存。

每个外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。

## 外设I/O端口读写

X86的cpu可以直接读写以下三个地方的数据,读写三个地方的指令都是不同的,他们的空间也是分开的,这点要注意。

I/O端口是指设备控制器中可被CPU直接访问的寄存器,主要有以下三类寄存器。

  • 数据寄存器:用于缓存从设备送来的输入数据,或从CPU送来的输出数据。
  • 状态寄存器:保存设备的执行结果或状态信息,以供CPU读取。
  • 控制寄存器:由CPU写入,以便启动命令或更改设备模式。

I/O 端口要想能够被CPU访问,就要对各个端口进行编址,每个端口对应一个端口地址。而对 I/O端口的编址方式有与存储器独立编址和统一编址两种.

将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的“ I/O端口”方式

端口读写指令

CPU对外设通过端口读写指令来进行读写数据。

端口读写指令

  • in cpu寄存器,端口地址 : 从端口中读取数据到CPU寄存器

  • out 端口地址 , cpu寄存器: 写入CPU寄存器的数据到端口中

  • 此处的CPU寄存器,为一个字节(8位),字(16位)或双字(32位),根据CPU寄存器的大小,从端口地址处读取不同的字节数。

include/stdint.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H

typedef signed char int8;
typedef signed short int16;
typedef signed int int32;
typedef signed long long int64;

typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef unsigned long long uint64;

#endif

类型别名

c语言调用汇编

定义外设端口读写方法,C语言调用汇编

1)首先定义一个C语言头文件

2)使用汇编实现C语言头文件方法

3)C语言和汇编编译链接在一起

include/io.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef __LIB_IO_H
#define __LIB_IO_H

#include "stdint.h"

void io_cli();

void io_sti();

void io_hlt();

uint8 io_in8(uint16 port);

void io_out8(uint16 port, uint8 data);

uint16 io_in16(uint16 port);

void io_out16(uint16 port, uint16 data);

uint32 io_in32(uint16 port);

void io_out32(uint16 port, uint32 data);

#endif

asm/io.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
;io.asm
[bits 32]

global io_cli, io_sti, io_hlt
global io_in8, io_out8
global io_in16, io_out16
global io_in32, io_out32

[section .text]

io_cli: ;void io_cli
cli
ret

io_sti: ;void io_sti
sti
ret

io_hlt: ;void io_hlt
hlt
ret

io_in8: ; uint8 io_in8(uint16 port)
mov dx,[esp+4]
mov al,0
in al,dx
ret

io_out8: ; void io_out8(uint16 port,uint8 data)
mov dx,[esp+4]
mov al,[esp+8]
out dx, al
ret

io_in16: ; uint16 io_in16(uint16 port)
mov dx,[esp+4]
mov ax,0
in ax,dx
ret

io_out16: ; void io_out16(uint16 port,uint16 data)
mov dx,[esp+4]
mov ax,[esp+8]
out dx, ax
ret

io_in32: ; uint32 io_in32(uint16 port)
mov dx,[esp+4]
mov eax,0
in eax,dx
ret

io_out32: ; void io_out32(uint16 port,uint32 data)
mov dx,[esp+4]
mov eax,[esp+8]
out dx,eax
ret



ELF文件

上次说过了,其实目标文件和可执行文件都是ELF格式文件

ELF索引表

ELF 文件包括三个索引表

  • ELF Header: ELF文件头

    作用:

    1. 指定程序入口

    2. 定位Program  header  table位置

    3. 定位Section  header  table位置

  • Program  header  table:程序头表。

    作用:

    1)查询segment的位置(一个segment可能会包含多个Section)

    2)根据此表创建内存中创建映像

  • Section  header  table:节区头表。

    作用:

    1)存储文件节区的信息

    2)根据此表定位代码段,数据段位置

ELF内容区

  • 字符串表:interp,.strtable ,.shstrtab,.dynstr节区
  • 符号表:.symtab
  • 代码段:.text节区
  • 数据段:.data、.rodata 、.bss节区
  • 全局偏移表:.got节区
  • 过程链接表: .plt节区
  • 哈希表:指.hash节区
  • 编译器版信息:.comment

ELF字段类型

  • Elf32_Addr:4字节,无符号程序地址

  • Elf32_Half: 2字节,无符号中等整数

  • Elf32_Off:4 字节,无符号文件偏移

  • Elf32_SWord:4 字节,有符号大整数

  • Elf32_Word :4字节,无符号大整数

ELF的三种Header格式

ELF Header格式

字段 类型 长度 说明
ident char [16] 16 魔数
type Elf32_Half 2 文件类型
machine Elf32_Half 2 硬件平台
version Elf32_Word 4 版本
entry Elf32_Addr 4 程序进入点 <24>
phoff Elf32_Off 4 程序头表偏移量<28>
shoff Elf32_Off 4 节头表偏移量
flags Elf32_Word 4 处理器特定标志
ehsize Elf32_Half 2 ELF头部大小
phentsize Elf32_Half 2 程序头大小<42>
phnum Elf32_Half 2 程序头数量<44>
shentsize Elf32_Half 2 节头大小
shnum Elf32_Half 2 节头数量
shstrndx Elf32_Half 2 字符串表索引节头

ELF文件分析

查看文件头

readelf -h loader1.bin

images/5_3_1.png

Program Header 程序头格式

字段 类型 长度 说明
type Elf32_Word 4 段类型
offset Elf32_Off 4 偏移量 <4>
vaddr Elf32_Addr 4 内存虚拟地址 <8>
paddr Elf32_Addr 4 物理地址
filesz Elf32_Word 4 段大小(文件占用)<16>
memsz Elf32_Word 4 段大小(内存占用)
flag Elf32_Word 4 段标志
align Elf32_Word 4 段对齐

查看程序头:

$ readelf -l 文件名

images/4_1_1.png

Section Header节头

节头格式

字段 类型 长度 说明
name Elf32_Word 4 节名称
type Elf32_Word 4 节类型
flags Elf32_Word 4 节属性
addr Elf32_Addr 4 节区地址
offset Elf32_Off 2 偏移量
size Elf32_Word 4 节大小(文件)
link Elf32_Word 4 节区头部表索引链接
info Elf32_Word 4 附加信息
addralign Elf32_Word 4 地址对齐
entsize Elf32_Word 4 项目表长度

查看字段头:

$ readelf -S 文件名

images/4_1_2.png

符号表 Symbol table

其他

  • 查看全部信息

readelf -a loaderELF.o

  • 查看所有分段大小

size loaderELF.o

  • 查看分段内容

objdump -s loaderELF.o

  • 查看符号表

objdump -t loaderELF.o

  • 反编译

objdump -S loaderELF.o

内存复制

数据传送指令

movsb指令

movsb : 即字符串传送指令,这条指令按字节送数据。默认复制一个字节/

参数:

  • esi:数据源地址
  • edi:数据目标地址

rep movsb指令

rep movsb : 即字符串传送指令,复制多个字节

参数:

  • esi:数据源地址
  • edi:数据目标地址
  • ecx:复制字节数、

汇编代码

1
2
3
4
5
6
7
8
9
10
;------------------    
;内存复制 : 源地址,目标地址,字节数
;入参:
; esi = 源地址
; edi = 目标地址
; ecx = 字节数
MemCopy:
rep movsb;
ret

解析执行ELF文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

;-----------------------------------
; 解析执行ELF文件: AnalyzeELF
; 入参:
; eax=文件内存位置
; 出参:
; ebx=入口地址
AnalyzeELF:


mov edx, 0
mov ecx, 0
mov ebx, [eax + 28] ;program header偏移量
add ebx, eax ;program header位置
mov dx, [eax + 42] ;program header大小
mov cx, [eax + 44] ;program header数量


.loopSegment:
cmp byte [ebx + 0],0 ;ptype为0,程序段未使用
je .nextSegment


push ecx;
mov ecx, 0;
;---------------
;复制segment
mov esi, [ebx + 4] ;segment偏移量
add esi, eax ;src
mov edi, [ebx + 8] ;dist
mov cx, [ebx + 16] ;len
call MemCopy
pop ecx;

.nextSegment:
add ebx, edx
loop .loopSegment ;继续读取下一个segment
mov ebx, [eax + 24] ;返回入口地址
ret

调用处的代码

1
2
3
4
5
6
7
;----------------------
;解析并执行ELF文件
AnalyzeKernel:
mov eax, KERNEL_BASE_ADDR
call AnalyzeELF
jmp ebx

执行的c语言程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int is_prime(unsigned short n)
{
//返回1表示素数,返回0表示非素数
int i = 0;
for (i = 2; i < n; i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}

int _start(){
unsigned short* pvga = (unsigned short*)0xb8000; //填充到显示内存的初始地址
for(int i = 0;i <= 0x7fff;i++){
//char: 0x3 ,color: 0x104
if(is_prime(i) == 1) {
*(pvga + i) = (unsigned short)0x1704; //显存填充,蓝色背景白色棱形
} else {
*(pvga + i) = (unsigned short)0x1700; //显存填充背景色
}
}
fin:
goto fin;
}


Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# tools
PLATFORM=Linux
NASM=nasm
BOCHS=bochs
BXIMAGE=bximage

# args
boot=boot
build=build
ENTRY_POINT=0x10000
CFLAGS=-m32 -c -nostdinc -nostdlib -fno-builtin -Wall -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS=-m elf_i386 -s -e _start -Ttext $(ENTRY_POINT)

target: $(build)/rastos.img
@echo "build img completed"

$(build)/rastos.img:$(build)/boot.bin $(build)/loader.bin $(build)/loaderELF.bin
$(BXIMAGE) -func=create -imgmode=flat -hd=16M -q $(build)/rastos.img
sleep 2
dd if=$(build)/boot.bin of=$(build)/rastos.img bs=512 count=1 conv=notrunc
dd if=$(build)/loader.bin of=$(build)/rastos.img bs=512 count=1 seek=1 conv=notrunc
dd if=$(build)/loaderELF.bin of=$(build)/rastos.img bs=512 count=30 seek=2 conv=notrunc


$(build)/loaderELF.bin: $(build)/loaderELF.o
$(LD) $(build)/loaderELF.o -o $(build)/loaderELF.bin $(LDFLAGS)

$(build)/loaderELF.o: $(boot)/loaderELF.c
$(CC) $(boot)/loaderELF.c -o $(build)/loaderELF.o $(CFLAGS)

$(build)/%.bin: $(boot)/%.asm
$(NASM) -f bin -o $(build)/$*.bin $(boot)/$*.asm

prepare: $(build)
@echo "prepare dir $(build)"
ifeq ($(build), $(wildcard $(build)))
@echo "build directory exist..."
else
mkdir -p $(build)
endif

clean:
@echo "clean dir $(build)"
rm -rf $(build)/*

platform:
@echo $(PLATFORM)

加载执行c语言程序的代码并执行

images/3_2_1.png

天空任鸟飞,海阔凭鱼跃。我们已经成功的从启动,到进入c语言的世界,接下来就是无限的可能性。

C语言程序

工具

首先,需要的工具软件列表:

  • gcc编译器:

编译C语言程序

1. 为什么没有main函数

main函数链接时需要一些系统库文件。而我们的系统目前并没有任何的系统库可以用,会导致报错。

所以此处不能使用main函数。

那么我们使用默认的入口_start符号(不设置,则默认为0)

准备工作

输出当前现存位置是否为素数的c语言代码

boot/loaderELF.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int is_prime(unsigned short n)
{
//返回1表示素数,返回0表示非素数
int i = 0;
for (i = 2; i < n; i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}

int _start(){
unsigned short* pvga = (unsigned short*)0xb8000; //填充到显示内存的初始地址
for(int i = 0;i <= 0x7fff;i++){
//char: 0x3 ,color: 0x104
if(is_prime(i) == 1) {
*(pvga + i) = (unsigned short)0x1704; //显存填充,蓝色背景白色棱形
} else {
*(pvga + i) = (unsigned short)0x1700; //显存填充背景色
}
}
fin:
goto fin;
}


说明:
1)计算素数数字,左上为0,位置从左到右并逐行增加,和显存的偏移量相同
2)使用简单的循环遍历计算当前位置的数字是否为素数
3)如果位置不为素数,输出空白,为素数输出棱形
3)0x1704: 0x17代表蓝色背景白色文字,0x04在ascii码里面是棱形```

编译成目标文件

$ gcc boot/loaderELF.c -m32 -c build/loaderELF.o

gcc编译参数

-O0: 无优化。编译器不会进行任何优化,生成的代码与源代码几乎完全相同。
-nostdinc: 不搜索默认路径头文件
-nostdlib: 不使用标准库
-fno-builtin: 不使用内建函数
-Wall
-Wstrict-prototypes
-Wmissing-prototypes

链接C语言程序

1. 为什么要指定程序入口
由于在保护模式下,我们默认加载到 0x10000处执行代码。所以,下载需要做的是
1)指定当前c语言程序的入口地址为0x10000
2)复制程序段的执行程序段到 0x10000

链接并指定程序入口

ld -m elf_i386 -s -Ttext 0x00010000 build/loaderELF.o -o build/loaderELF.bin

最后的bin文件大小大概 是14Kb.

ELF信息查看

查看文件信息

file loaderELF.bin

images/3_1_1.png

查看反编译内容

objdump -S loaderELF.bin

images/3_1_2.png

查看文件信息

readelf -e loaderELF.bin

images/3_1_3.png

查看纯二进制内容

xxd loaderELF.bin

images/3_1_4.png

GDT全局描述符表

什么是GDT全局描述符表

GDT全称为Global Descriptor Table,全局描述符表。

保护模式的寻址方式不在使用寄存器分段的方式直接寻址方式了。而采用的是使用GDT(全局分段描述表)来寻址。从而使用更多的内存地址。

创建GDT全局描述符表使用到一个48位的寄存器:GDTR寄存器。

1)首先,在内存中划分一些内存段,并且每个内存段赋予一个索引。

2)然后,使用lgdt指令,设置GDT的索引和表信息的内存地址到GDTR寄存器。

3)进入保护模式,指令跳转,从实模式分段方式寻址切换到使用GDT分段方式寻址。

  1. GDT可以被放在内存的任何地方,只要提供内存地址给GDTR寄存器就可以了。

GDT格式

GDT全局描述符表

  • 表基地址,表基地址位GDT段表在内存的地址,GDT段表是一个列表,存储了多个 GDT段描述符。
  • 表界限:GDT段表的空间信息,以字节为单位。

images/2_7_1.png

GDT全局描述符表 = GDT段表基地址 | 16位表界限

GDT段表 =  GDT段描述符 |  GDT段描述符 | GDT段描述符 …

表界限 = GDT字节数 - 1 (表示 0 - 0x…)

GDT段描述符

images/2_7_2.png

GDT段描述符,用来描述在GDT方式在内存中分配的一个段信息,总共8字节64位。

GDT段描述符结构

为了兼容以前的CPU,GDT段描述符的信息被分割成几个部分,格式如下:

GDT段描述符 = 

高32位:段基址 (高8位)| 段描述符(高4位) | 段界限(高4位) | 段描述符(低8位)| 段基址 (中8位)

低32位:段基址 (低16位) | 段界限(低16位)

  • 32位段基址 = 段基址 (高8位) + 段基址 (中8位) + 段基址 (低16位)

  • 20段界限 = 段界限(高4位) + 段界限(低16位)

  • 12位段描述符 = 段描述符(高4位 )+ 段描述符(低8位)

段描述符定义

  • 段基址:规定段的起始地址,长度32位.
  • 段界限:规定段的大小,长度20位。段界限可以是以4KB或者1B为单元大小
  • 段属性:确定段的各种性质.长度(12位)

段属性:

  • G 粒度位: 段界限的单位大小,G=1表示段界限以4KB为单元单位,G=0表示段界限以1B为单元单位
  • D/B 表示操作数为多少位, 0表示16位操作数,1表示32位操作数
  • L : 0 表示非64位代码段,1表示64位代码段
  • AVL :可用字段,暂时没什么用
  • P 段存在位:通常为1,表示段存在于内存中,0则此段为非法的,不能被用来实现地址转换
  • DPL 特权级(2位): 用来实现保护机制
  • S 为0表示系统段,为1表示非系统段
  • type 类型(4位): 用于区别不同类型的描述符。内存段或者门的子类型

type值

Type位 说明 取值
代码段时
X:3位 代码段值为1 0:为数据段
1:为代码段
C:2位 访问位 0:为普通段
1:为一致码段
R:1位 是否可读 0:只执行
1:可读
A:0位 访问位. 该段是否被访问过 0 :未访问
1:已访问
数据段时
X:3位 数据段值为1 0:为数据段
1:为代码段
E:2位 扩展方向 0:向高位扩展
1:向低位扩展
W:1位 是否可写 0:只读
1:可写
A:0位 访问位 0: 未访问
1:已访问

段界限:

段界限边界值 = (描述符的段界限值 + 1) × (段界限颗粒读:4Kb 或者 1b) -1

反之:
描述符的段界限值 = (段界限边界值 + 1) /(段界限颗粒读:4Kb 或者 1b)

例如:

16MB的段界限值 = 0x1000000 /(段界限颗粒读:4Kb 或者 1b - 1)= 0x0fff

段选择子

段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)

GDTR寄存器

在内存中建立完成GDT信息后,CPU会将GDT的内存地址 和 段界限 数据加载入GDTR寄存器

GDTR寄存器数据(48位):

GDTR定义数据(48位) = GDT全局描述符表的大小(16位) + GDT全局描述符表的地址(32位)

lgdt指令

lgdt GDTR定义数据

其中GDT全局描述符表数据格式如下

GDT全局描述符表 = GDT段描述符(64位) | GDT段描述符(64位) | GDT段描述符(64位) …

GDT段描述符 = 段基址 (8位)| 段描述符(4位) | 段界限(4位) | 段描述符(8位) | 段基址 (8位) | 段基址 (16位) | 段界限(16位)

其中,第一个GDT段的数据为空。

GDT临时分段

GDT临时段说明

现在已经进入了保护模式, 目前的改变

  • 可以访问1M以上的内存了
  • 可以使用32位的指令

问题:

由于以前的是实式下段寄存器寻址方式无法使用了,我们必须切换到使用GDT段方式来寻址

首要的任务就是先建立一个临时的GDT段,以便我们接下来的指令操作

目前准备建立3个段,如下:

Base, Limit, Attr

代码段:0x00000000, 0xfffff, 1100_1001_1010B = db 0x0000ffff, 0x00cf9a00

数据段:0x00000000, 0xfffff, 1100_1001_0010B = db 0x0000ffff, 0x00cf9200

vga显卡内存数据段:x000b8000, 0x00fff, 1100_1001_0010B

GDT解析宏

首先创建一个nasm宏,可以进行GDT解析

参数为GDT的Base, Limit, Attr,也就是段基址(32位),段界限(20位),段描述符(12位),然后生成GDT在内存中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;---------------------------------------------------------
; 描述符
; usage: Gdt_Descriptor Base, Limit, Attr : 段基址(32位),段界限(20位),段描述符(12位)
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
;---------------------------------------------------------
%macro Gdt_Descriptor 3
dw %2 & 0xFFFF
dw %1 & 0xFFFF
db (%1 >> 16) & 0xFF
db %3 & 0xFF
db ((%3 >> 4 ) & 0xF0 ) | ((%2 >> 16) & 0x0F )
db (%1 >> 24) & 0xFF
%endmacro

GDT 描述符属性定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
;--------------   gdt描述符属性  -------------
DESC_G_4K equ 1000_0000_0000b
DESC_D_32 equ 0100_0000_0000b
DESC_L equ 0000_0000_0000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0000_0000_0000b ; cpu不用此位,暂置为0
DESC_P equ 0000_1000_0000b
DESC_DPL_0 equ 000_0000b
DESC_DPL_1 equ 010_0000b
DESC_DPL_2 equ 100_0000b
DESC_DPL_3 equ 110_0000b
DESC_S_CODE equ 1_0000b
DESC_S_DATA equ 1_0000b
DESC_S_SYS equ 0_0000b
DESC_TYPE_CODE equ 1010b ;x=1可执行代码段,c=0普通,r=1可读,a=0已访问位a清0
DESC_TYPE_DATA equ 0010b ;x=0数据段,e=0向高位扩展,w=1可写,a=0已访问位a清0.

DESC_ATTR_CODE equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE
DESC_ATTR_DATA equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA


;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

;-------------    选择子序列 --------------------------
SELECTOR_CODE equ 0x1<<3
SELECTOR_DATA equ 0x2<<3
SELECTOR_VGA equ  0x3<<3

定义GDT全局描述符表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;---------------------------------
;定义GDT全局描述符表
;code: 0x00000000 - 0x000FFFFF
;data: 0x00000000 - 0x000FFFFF
;vga: 0x000B8000 - 0x000BFFFF
;---------------------------------
Gdt_Addr:
dw 8*4-1 ;指定段上限为4(GDT全局描述符表的大小)
dd Gdt_Table_Addr ;GDT全局描述符表的地址
Gdt_Table_Addr:
Gdt_Descriptor 0,0,0
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_CODE ;可以执行的段
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_DATA ;可以读写的段
Gdt_Descriptor 0x000B8000, 0x00007FFF, DESC_ATTR_DATA ;vga段
dw 0

加载gdt

1
2
3
;---------------------------
;加载GDT
lgdt [Gdt_Addr]

代码

创建常量头文件

创建 boot.inc文件。用来配置常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
; boot.inc
;---------------------------------------------------------
; 描述符
; usage: Gdt_Descriptor Base, Limit, Attr : 段基址(32位),段界限(20位),段描述符(12位)
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
;---------------------------------------------------------
%macro Gdt_Descriptor 3
dw %2 & 0xFFFF
dw %1 & 0xFFFF
db (%1 >> 16) & 0xFF
db %3 & 0xFF
db ((%3 >> 4 ) & 0xF0 ) | ((%2 >> 16) & 0x0F )
db (%1 >> 24) & 0xFF
%endmacro



;-------------- gdt描述符属性 -------------
DESC_G_4K equ 1000_0000_0000b
DESC_D_32 equ 0100_0000_0000b
DESC_L equ 0000_0000_0000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0000_0000_0000b ; cpu不用此位,暂置为0
DESC_P equ 0000_1000_0000b
DESC_DPL_0 equ 000_0000b
DESC_DPL_1 equ 010_0000b
DESC_DPL_2 equ 100_0000b
DESC_DPL_3 equ 110_0000b
DESC_S_CODE equ 1_0000b
DESC_S_DATA equ 1_0000b
DESC_S_SYS equ 0_0000b
DESC_TYPE_CODE equ 1010b ;x=1可执行代码段,c=0普通,r=1可读,a=0已访问位a清0
DESC_TYPE_DATA equ 0010b ;x=0数据段,e=0向高位扩展,w=1可写,a=0已访问位a清0.

DESC_ATTR_CODE equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE
DESC_ATTR_DATA equ DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA


;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

;-------------    选择子序列 --------------------------
SELECTOR_CODE equ 0x1<<3
SELECTOR_DATA equ 0x2<<3
SELECTOR_VGA equ  0x3<<3

;----------- loader const ------------------
LOADER_SECTOR_LBA equ 0x1 ;第2个逻辑扇区开始
LOADER_SECTOR_NUM equ 9 ;读取9个扇区
LOADER_BASE_ADDR equ 0x8000 ;内存地址0x8000
;-------------------------------------------

loader.asm文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
;RAST LOADER [0x9000]
;Tab=4
[bits 16]

%include "boot/boot.inc"

section loader vstart=LOADER_BASE_ADDR ;指明程序的偏移的基地址


jmp Entry;
;---------------------------------
;定义GDT全局描述符表
;code: 0x00000000 - 0x000FFFFF
;data: 0x00000000 - 0x000FFFFF
;vga: 0x000B8000 - 0x000BFFFF
;---------------------------------
Gdt_Addr:
dw 8*4-1 ;指定段上限为4(GDT全局描述符表的大小)
dd Gdt_Table_Addr ;GDT全局描述符表的地址
Gdt_Table_Addr:
Gdt_Descriptor 0,0,0
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_CODE ;可以执行的段
Gdt_Descriptor 0x00000000, 0x000FFFFF, DESC_ATTR_DATA ;可以读写的段
Gdt_Descriptor 0x000B8000, 0x00007FFF, DESC_ATTR_DATA ;vga段
dw 0

;程序核心内容
Entry:

;----------------------
;禁止CPU级别的中断,进入保护模式时没有建立中断表
;----------------------
cli

;----------------------
;打开A20
;----------------------
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al

;----------------------
;加载GDT
;----------------------
lgdt [Gdt_Addr]

;----------------------
;进入保护模式
;----------------------
mov eax,cr0
or eax,0x1 ;设置第0位为1
mov cr0,eax

;程序挂起
Fin:
hlt ;让CPU挂起,等待指令。
jmp Fin



times 512-($-$$) db 0 ; 处理当前行$至结束(1FE)的填充

测试

使用bochs执行

打好断点后,执行并查看gtd描述符数据是否正确。

info gdt

images/2_7_3.png

保护模式

保护模式寻址方式

1. 实模式下寻址的缺陷

2.保护模式下寻址

实模式下使用的是段寄存器(16位) << 4 + 偏移地址方式来寻址。

保护模式和实模式下的完全不一样。

  1. 保护模式下寻址方式

    1)首先内存中建立一个GDT全局分段描述表。

    2)DS中不再是内存的段开始地址,而是GDT表的索引。

    3)寻址时,首先根据DS的高13位的值得到一个索引,然后查找到在GDT中对应的一个全局分段描述。再根据这个描述来定位到段的开始位置。

    4)找到GDT段之后,继续根据偏移地址,在GDT段内进行内存寻址。

  2. 段的跳转指令

    JMP 段选择子:偏移地址

1)加载段选择子到CS段寄存器
2)获取段选择子索引号,根据索引号查找GDT表,加载GDT段描述符到CS段寄存器的描述符缓存
3)加载偏移地址到EIP寄存器
4)根据 CS段寄存器的描述符缓存 和 EIP寄存器 寻址。

最后跳转位置是

内存地址:段基本地址(根据段选择子获取) + 偏移地址

保护模式下的段

  1. 段选择器

保护模式下,段寄存器CS,DS,ES,,FS,GS,SS,称之为段选择器。

段选择器中的数据称为段选择子

段选择子 : 描述符索引(13位) | TI | RPL

其中

  • 描述符索引:GDT描述符表中的描述符的索引号(从0开始:0,1,2,3…)
  • TI: TI = 0 表示GDT描述符, TI =1 表示LDT描述符
  • RPL: 请求权特级:

当我们跳转到段的时侯,实际上是段选择器赋值为以上格式的值即可。

images/003.png

实模式和保护模式的区别

在计算机加载完成后,在实模式执行完一些初始化和加载工作。然后CPU设置进入保护模式。可以使用16位的数据。。

为了突破实模式1M内存寻址的限制,使用到更多内存。于是出现了保护模式,保护模式下,通过开启A20总线,可以使用32位的寄存器操作,其实访问地址已经达到了1<<32=4G内存。

实模式与保护模式的最大区别就是寻址方式:

1)保护模式不再使用段寄存器 <<4 +偏移地址的方式寻址,通过建立分段表将内存分成段。寻址时先加载分段表进入不同的段位置,然后在当前段内继续进行内存寻址。

2)保护模式不能使用BIOS中断

进入保护模式

[TOC]

进入保护模式

进入保护模式的步骤:

  1. 关闭中断,打开地址线A20GATE,使得CPU可以访问1M以上的内存空间。
  2. 设置CR0寄存器,进入保护模式。
  3. 加载临时GDT
  4. 进入保护模式后,首先执行jmp指令。因为内存寻址方式改变,需要刷新指令流水线

打开A20Gate

1. A20Gate的作用

在实模式下,A20Gate是关闭的,意味着只能使用20根地址线,需要通过打开A20Gate,访问第21根以上的总线。

A20Gate关闭时侯的内存访问:

在8088 CPU下, 只能使用20根总线 .

因此,初始化时, A20Gate关闭式,使用20根总线. 所以寻址范围位 0x00000 ~ 0xFFFFF,总共1M的地址范围。

当访问的地址大于这个范围,高位的值将被截取掉,导致超出1M的地址访问会使得CPU回滚到1M内地址范围的现象

例如:

当使用 [0xFFFF :0xFFFF ] 内存地址,得到的地址位 0x10FFEF 。但是在实模式下,由于20根总线的限制,最高位的1是无效的,实际的访问地址回绕到 [0x0FFEF]。

A20Gate打开后的内存访问:

后期,80286使用24根总线,而80286有24根总线,80386有32位总线.

打开A20Gate, 可以使用到32位的地址总线,内存地址访问也达到了1<<32 的4G范围。

实际上开启A20Gate,总线的寻址能力达到了4G,但是cpu的内存访问能力因为16位段寄存器,和16位偏移地址的限制,并不能协调工作。

A20Gate打开后, 还需要进入保护模式, 建立GDT描述名, 进行段地址 和 内存的映射关系, 使用新的内存地址访问方式 . 突破cpu的内存访问限制。

2. 开启A20Gate

开启A20Gate,只要设置io端口0x92的第一位为1就可以了。

1
2
3
4
5
6
;------------------
;打开A20
cli ;禁止CPU级别的中断
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al

设置CR0寄存器,进入保护模式

CR0寄存器

images/2_1_3.png

CR0寄存器是一个32位的寄存器

第0位-PE位:
Protection Enable(保护使能)是CR0寄存器的第0位(bit 0)。当设置了PE标志时,启用保护模式;当清除PE标志时,启用实地址模式。该标志位并不直接启用分页机制,它仅启用段级别的保护。要启用分页,必须同时设置PE和PG标志。

第1位-MP位:
Monitor Coprocessor(监视协处理器)是CR0寄存器的第1位(bit 1)。它控制WAIT(或FWAIT)指令与TS标志(CR0的第3位)的交互作用。如果设置了MP标志,并且TS标志也被设置,那么WAIT指令将引发设备不可用异常(#NM)。如果清除了MP标志,则WAIT指令将忽略TS标志的设置。

第16位-WP位:
Write Protect(写保护)是CR0寄存器的第16位(bit 16)。当设置了WP标志时,阻止特权级程序对只读页面进行写操作;当清除WP标志时,允许特权级程序对只读页面进行写操作(不考虑U/S位的设置)。该标志有助于实现创建新进程(forking)时使用的写时复制(copy-on-write)方法,该方法在UNIX等操作系统中被使用。在软件可以设置CR4.CET之前,必须设置此标志,并且只要CR4.CET = 1,就不能清除该标志。

第18位-AM位:
Alignment Mask(对齐掩码)是CR0寄存器的第18位(bit 18)。当设置了AM标志时,启用自动对齐检查;当清除AM标志时,禁用对齐检查。对齐检查仅在满足以下条件时进行:AM标志被设置、EFLAGS寄存器中的AC标志被设置、CPL(当前特权级)为3,并且处理器处于保护模式或虚拟8086模式下。

第31位-PG位:
Paging(分页)标志位是CR0寄存器的第31位。当该位被设置时(为1时),启用分页机制;当该位被清除时(为0时),禁用分页机制。当分页被禁用时,所有线性地址都被视为物理地址。也就是说没有分页机制,段机制通过段基址加偏移后就是真实物理地址,如果PE标志(寄存器CR0的第0位)未设置,PG标志则没有影响;在PE标志被清除时设置PG标志会导致通用保护异常(#GP)。

设置CR0寄存器的最高位为0,最低位为1,则可以进入保护模式。

CR0寄存器的作用

  • 改变段寻址方式,使用段描述符方式寻址。
  • 实模式指令的操作数默认为16位,保护模式指令的操作数默认为32位。

代码:

1
2
3
4
5
;------------------
;进入保护模式
mov eax,CR0
or eax,0x00000001 ;设置第0位为1
mov CR0,eax

loader.asm完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
; RAST LOADER
;Tab=4
[bits 16]

;----------- loader const ------------------
LOADER_BASE_ADDR equ 0x9000 ;内存地址0x9000
;---------------------------------------

section loader vstart=LOADER_BASE_ADDR ;指明程序的偏移的基地址

jmp Entry


;程序核心内容
Entry:


;------------------
;禁止CPU级别的中断
;------------------
cli

;------------------
;打开A20
;------------------
in al,0x92
or al,0000_0010B ;设置第1位为1
out 0x92,al


;------------------
;进入保护模式
;------------------
mov eax,cr0
or eax,0x1 ;设置第0位为1
mov cr0,eax



;程序挂起
jmp $ ;让CPU挂起,等待指令。

times 512-($-$$) db 0 ; 处理当前行$至结束(1FE)的填充

使用bochs调试

在0x7c00打断点,输入c跳转执行

$ pb 0x7c00

$ c

输入显示切换模式命令

$ show mode

输入c继续执行

$ c

可以看到控制他输出:

00017609546: switched from ‘real mode’ to ‘protected mode’

说明系统成功的从实模式切换到保护模式

images/2_6_1

查看CR0的PE位: 值为1

$ creg

images/2_6_1

保护模式内存分配

内存区域 大小 数据内容 说明
0x7C00 - 0x7DFF 512B boot.bin 引导扇区的内存地址
0x8000 - 0x81FF 512B loader.bin loader内存位置
0x8200 - 0xC200 16KB kernel.bin kernel内存位置
0x10000 - 0x8FFFF 空白(执行loader程序)
0x90000 - 0x9FFFF 系统信息 内存信息
0xA0000 - 0xFFFFF 显示 显存地址(默认使用0xB8000 - 0xBFFFF)
0x100000-0x101000 4KB 页目录 1MB内存位置
0x101000-0x200000 页表 页表信息

Makefile

安装make

安装make

sudo apt-get install make

make -v

创建Makefile文件,并执行make命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# tools
PLATFORM=Linux
NASM=nasm
BOCHS=bochs
BXIMG=bximage

# args
boot=boot
build=build

target: prepare img
$(BOCHS) -f bochsrc.me


img: $(build)/astraos.img
@echo "build img completed"

$(build)/astraos.img:$(build)/boot.bin $(build)/loader.bin
$(BXIMAGE) -mode=create -imgmode=flat -hd=16M -q $(build)/astraos.img
sleep 1
dd if=$(build)/boot.bin of=$(build)/astraos.img bs=512 count=1 conv=notrunc
dd if=$(build)/loader.bin of=$(build)/astraos.img bs=512 count=1 seek=1 conv=notrunc

$(build)/%.bin: $(boot)/%.asm
$(NASM) -f bin -o $(build)/$*.bin $(boot)/$*.asm

prepare: $(build)
@echo "prepare dir $(build)"
ifeq ($(build), $(wildcard $(build)))
@echo "build directory exist..."
else
mkdir -p $(build)
endif

clean:
@echo "clean dir $(build)"
rm -rf $(build)/*

platform:
@echo $(PLATFORM)

0%